﻿using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Sgr.BillNumbering.Domain;
using Sgr.Exceptions;
using System.Collections.Concurrent;

namespace Sgr.BillNumbering.Services.Impl
{
    public class BufferedCodeSequence<TDbContext> : ICodeSequence where TDbContext : DbContext
    {
        private int _cleanupRunning = 0;
        private long _lastCleanupTicks = 0;   // 上次清理时间（ticks，用于节流）

        private readonly Random _random = Random.Shared;
        private readonly ConcurrentDictionary<SequenceKey, SequenceSegment> _segments = new();
        private readonly CodeOptions _options;
        private readonly IServiceScopeFactory _scopeFactory;
        private readonly ILogger<BufferedCodeSequence<TDbContext>> _logger;

        // 锁分片，固定数量，避免每个Key都分配锁
        private static readonly SemaphoreSlim[] _lockStripes =
            Enumerable.Range(0, 256).Select(_ => new SemaphoreSlim(1, 1)).ToArray();

        private static SemaphoreSlim GetStripeLock(in SequenceKey key)
        {
            //int hash = HashCode.Combine(key.Category, key.TimeKey, key.CustomPrefix);
            int hash = HashCode.Combine(key.Category, key.CustomPrefix);

            //0x7FFFFFFF 是 int 的最大正数（2147483647），二进制为 0111 1111 1111 1111 1111 1111 1111 1111。
            //hash & 0x7FFFFFFF 把 hash 的最高位（符号位）清零，保证结果为非负整数。
            //这样 % _lockStripes.Length 得到的 idx 一定在 [0, _lockStripes.Length-1] 之间，安全用于数组下标。
            int idx = (hash & 0x7FFFFFFF) % _lockStripes.Length;

            return _lockStripes[idx];
        }

        public BufferedCodeSequence(
            IServiceScopeFactory scopeFactory,
            IOptions<CodeOptions> options,
            ILogger<BufferedCodeSequence<TDbContext>> logger)
        {
            _scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory));
            _options = options?.Value ?? throw new ArgumentNullException(nameof(options));
            _logger = logger ?? throw new ArgumentNullException(nameof(logger));
        }

        public async Task<long> GetNextValueAsync(
            string category,
            string customPrefix = "",
            SequenceResetStrategy resetStrategy = SequenceResetStrategy.Never,
            int? segmentSize = null,
            CancellationToken cancellationToken = default)
        {
            if (string.IsNullOrWhiteSpace(category))
                throw new ArgumentException("Category is required.", nameof(category));

            if (!segmentSize.HasValue)
                segmentSize = _options.DefaultSegmentSize;

            var now = DateTime.UtcNow;
            var timeKey = TimeKeyGenerator.Generate(resetStrategy, now);
            var key = new SequenceKey(category, timeKey, customPrefix);

            // 快路径：已有段直接取
            if (_segments.TryGetValue(key, out var segment))
            {
                if (segment.TryGetNext(out var value))
                {
                    TryLazyCleanup();
                    return value;
                }
            }

            // 分配新段时进入分片锁
            var stripeLock = GetStripeLock(key);
            await stripeLock.WaitAsync(cancellationToken);
            try
            {
                // 双重检查，锁等待期间可能已有其他线程完成分配
                if (_segments.TryGetValue(key, out segment) && segment.TryGetNext(out var lockedRetryValue))
                    return lockedRetryValue;

                var newSegment = await AllocateNewSegmentAsync(key, segmentSize.Value, resetStrategy, now, cancellationToken);
                _segments[key] = newSegment;

                return newSegment.TryGetNext(out var firstValue)
                    ? firstValue
                    : throw new InvalidOperationException("Failed to get first value from new segment.");
            }
            finally
            {
                stripeLock.Release();
            }
        }

        private async Task<SequenceSegment> AllocateNewSegmentAsync(
            SequenceKey key,
            int segmentSize,
            SequenceResetStrategy resetStrategy,
            DateTime now,
            CancellationToken ct)
        {
            const int maxAttempts = 5;
            var expiry = TimeKeyGenerator.CalculateExpiry(resetStrategy, now);

            if (segmentSize <= 0)
                segmentSize = 1;

            for (var attempt = 0; attempt < maxAttempts; attempt++)
            {
                ct.ThrowIfCancellationRequested();

                try
                {
                    using var scope = _scopeFactory.CreateScope();
                    await using var dbContext = scope.ServiceProvider.GetRequiredService<TDbContext>();

                    var counter = await dbContext.Set<SequenceCounter>()
                        .Where(c => c.Category == key.Category
                                 && c.TimeKey == key.TimeKey
                                 && c.CustomPrefix == key.CustomPrefix)
                        .FirstOrDefaultAsync(ct);

                    if (counter != null)
                    {
                        var newMax = counter.AllocatedMaxValue + segmentSize;
                        if (newMax > counter.MaxValue)
                            throw new BusinessException("Sequence counter has reached its maximum value.");

                        counter.AllocatedMaxValue = newMax;
                        counter.UpdatedAt = now;
                        counter.RowVersion++;
                    }
                    else
                    {
                        counter = new SequenceCounter
                        {
                            Category = key.Category,
                            TimeKey = key.TimeKey,
                            CustomPrefix = key.CustomPrefix,
                            AllocatedMaxValue = segmentSize,
                            MaxValue = int.MaxValue,
                            UpdatedAt = now,
                            ExpiryTime = expiry,
                            RowVersion = 1
                        };
                        await dbContext.Set<SequenceCounter>().AddAsync(counter, ct);
                    }

                    var affected = await dbContext.SaveChangesAsync(ct);
                    if (affected == 0)
                        continue;

                    var startValue = counter.AllocatedMaxValue - segmentSize;
                    return new SequenceSegment(startValue, counter.AllocatedMaxValue, expiry);
                }
                catch (DbUpdateException ex) when (attempt < maxAttempts - 1)
                {
                    _logger.LogWarning(ex,
                        "Concurrency conflict allocating segment for {Category}-{TimeKey}-{CustomPrefix}, attempt {Attempt}",
                        key.Category, key.TimeKey, key.CustomPrefix, attempt + 1);
                    await Task.Delay(50 * (attempt + 1), ct);
                }
            }

            throw new BusinessException("Failed to allocate sequence segment after concurrency retries.");
        }

        public int CleanupExpiredSegments()
        {
            var now = DateTime.UtcNow;

            // 节流：最小清理间隔
            var last = Interlocked.Read(ref _lastCleanupTicks);
            var minIntervalTicks = TimeSpan.FromMilliseconds(_options.CleanupMinIntervalMs).Ticks;
            if (last != 0 && (now.Ticks - last) < minIntervalTicks)
                return 0;

            Interlocked.Exchange(ref _lastCleanupTicks, now.Ticks);

            int removed = 0;
            var toRemove = new List<SequenceKey>();

            foreach (var kvp in _segments)
            {
                if (removed >= _options.CleanupMaxRemovePerRound)
                    break;

                var segment = kvp.Value;
                if (segment.ExpiryTime.HasValue && now >= segment.ExpiryTime.Value)
                {
                    toRemove.Add(kvp.Key);
                    removed++;
                }
            }

            foreach (var key in toRemove)
            {
                _segments.TryRemove(key, out _);
            }

            return toRemove.Count;
        }

        private void TryLazyCleanup()
        {
            if (_segments.Count <= _options.LazyCleanupThreshold) return;
            if (_random.Next(100) != 1) return;
            if (Interlocked.CompareExchange(ref _cleanupRunning, 1, 0) != 0) return;

            int beforeCount = _segments.Count;
            int removed = 0;

            try
            {
                removed = CleanupExpiredSegments();

                // 记录详细清理信息
                _logger.LogInformation(
                    "Lazy cleanup executed. CachedKeysBefore={BeforeCount}, Removed={Removed}",
                    beforeCount,
                    removed);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex,
                    "Lazy cleanup failed. CachedKeysBefore={BeforeCount}, Removed={RemovedSoFar}",
                    beforeCount,
                    removed);
            }
            finally
            {
                Interlocked.Exchange(ref _cleanupRunning, 0);
            }
        }

        private sealed class SequenceSegment
        {
            private long _currentValue;
            public long MaxValue { get; }
            public DateTime? ExpiryTime { get; }

            public SequenceSegment(long startValue, long maxValue, DateTime? expiryTime)
            {
                if (maxValue < startValue)
                    throw new ArgumentOutOfRangeException(nameof(maxValue), "maxValue 必须 >= startValue");

                _currentValue = startValue;
                MaxValue = maxValue;
                ExpiryTime = expiryTime;
            }

            public bool TryGetNext(out long value)
            {
                var current = Interlocked.Increment(ref _currentValue);
                if (current <= MaxValue)
                {
                    value = current;
                    return true;
                }
                value = default;
                return false;
            }
        }

        private readonly record struct SequenceKey(
            string Category,
            string TimeKey,
            string CustomPrefix);
    }
}